Local Projections ================== Description ------------ A local projection estimates impulse responses by regressing a *lead* of a variable on current and lagged variables, one regression per horizon (Jordà, 2005):: y_{i,t+k} = b'_{i,k,0} y_t + sum_l b'_{i,k,l} y_{t-l} + C'_{i,k} w_t + u_{i,k,t} It is **not** a VAR. There is no dynamic system, no state space and no likelihood: it is a stack of ordinary least-squares regressions, and nothing about it is sampled. It also is not a univariate model — at each horizon the *same* design matrix serves every equation, so the horizon-``k`` coefficients form an ``n x n`` block. That block is what makes the object structurally useful: with ``y_t`` on the right-hand side, **the coefficient block on** ``y_t`` **at horizon k is the reduced-form impulse response matrix at that horizon** (Plagborg-Møller and Wolf, 2021). So a local projection produces exactly the pair that a rotation-based identification scheme consumes, .. math:: \{B_{k}\}_{k=0}^{K}, \qquad \Sigma and everything structural happens afterwards. This is why RISE ships local projections as two functions rather than as a model class: there is no model object to build. .. list-table:: :header-rows: 1 :widths: 40 60 * - Function - Does * - ``rise.engine.var.lptools.estimate`` - the projections; returns ``B`` and ``Sigma``, or ``beta`` and ``beta_se`` when given a measured shock * - ``rise.engine.var.lptools.identified_set`` - sign-restricted identified sets by random rotations Quick-start ------------ :: endo = {'FFR','GDP','PCEG','SPARE','TIGHT','IMPP'}; est = rise.engine.var.lptools.estimate(db, endo, ... horizons = 0:12, ... lag_length = 1, ... constant_term = true, ... deterministic_vars = {'trend'}); ``db`` is a struct with one field per series. ``est.B{1}{k+1}`` is the ``n x n`` response matrix at horizon ``k``; ``est.Sigma`` is the innovation covariance. Two properties of the specification are worth knowing before reading any output: * **Horizon 0 is the identity by construction.** At ``k = 0`` the left-hand side sits on the right-hand side too, so the fit is exact and ``B{r}{1} = I``. It is set, not estimated. * **Therefore** ``Sigma`` **comes from the** ``k = 1`` **residuals**, not from ``k = 0``. State-dependent projections ---------------------------- Pass a ``state`` and the regressors are interacted with an indicator (Ramey and Zubairy, 2018):: est = rise.engine.var.lptools.estimate(db, endo, ... horizons = 0:6, ... state = struct('variable','ACR', ... 'value','median', ... % or a number 'delay',1)); The indicator is :math:`I_{t-1} = \mathbb{1}\{z_{t-d} > \bar z\}` with ``delay`` giving ``d``. ``est.B{1}`` is the block where the indicator is one; ``est.B{2}`` where it is zero. ``value = 'median'`` uses the sample median of the state variable. .. important:: With ``B_{r,0} = I`` in both states and a **single pooled** ``Sigma``, a state-dependent local projection can only reveal state-dependence that lives in the **propagation** coefficients, at ``k >= 1``. It is structurally blind to state-dependence that lives only in the impact covariance. If that is where your mechanism is, use a switching VAR instead (see :doc:`Main Reduced form VAR Modeling`). Two modes: measured shock, or rotation --------------------------------------- There are two quite different things called a local projection, and ``estimate`` does both. Which one you are running is recorded in ``est.mode``. .. list-table:: :header-rows: 1 :widths: 18 34 48 * - ``est.mode`` - When - What comes out * - ``'lpvar'`` - default - ``B`` — *reduced-form* responses. A structural shock exists only after you rotate; see below. * - ``'shock'`` - you pass ``shock`` - ``beta``, ``beta_se`` — *already structural*. No rotation. **lpvar mode** is the Plagborg-Møller & Wolf form used above: ``y_t`` is a regressor and the horizon-``k`` block on it is the reduced-form impulse response matrix, the same object a VAR's IRF is built from. Its entries answer "if :math:`y_j` moves unexpectedly by one unit today, where is :math:`y_i` in ``k`` periods?" — a forecast-revision question. Since the innovations are correlated, ":math:`y_j` moves alone" is not something the system can do, which is exactly why a structural reading requires the rotation step. **shock mode** is the Jordà (2005) / Ramey–Zubairy (2018) form. You supply a *measured* shock series — narrative, high-frequency surprise, proxy — and read its coefficient directly:: est = rise.engine.var.lptools.estimate(db, endo, ... horizons = 0:12, ... lag_length = 1, ... shock = 'mp_surprise', ... shock_lags = 4); est.beta{1}(i,k+1) % response of variable i at horizon k est.beta_se{1}(i,k+1) % Newey-West standard error :math:`\beta_{i,k}` **is** the structural response — no rotation, no identified set. The entire identifying assumption lives in the shock series, imported from outside the regression. .. important:: **shock mode drops the contemporaneous block, and must.** In shock mode ``y_t`` is *not* a regressor; only lags of ``y`` are controls. This is not an oversight. :math:`s_t` causes :math:`y_t`, so :math:`y_t` mediates the whole effect being measured — conditioning on it closes the channel and drives :math:`\beta` toward zero. It is a bad control, and a silent one: you would get small, tidy, badly wrong coefficients. Because there is no contemporaneous block, ``B`` is empty in shock mode and ``identified_set`` refuses such an estimate outright. Note also that ``beta`` at ``k = 0`` is a genuine estimate — the impact response — whereas in lpvar mode ``B{r}{1}`` is the identity by construction and carries no information. .. warning:: **Use the HAC standard errors.** Overlapping horizons make the horizon-``k`` residual serially correlated of order roughly ``k``, so OLS standard errors are wrong and get worse as the horizon grows. ``beta_se`` is Newey–West, truncated at ``k + 1`` by default; override with ``hac_lags``. lpvar mode reports no standard errors at all — its uncertainty is an identified *set*, not a band. Identification --------------- This section applies to **lpvar mode only**; a shock-mode estimate is already structural. Restrictions use the **same syntax as the VAR family** — they are parsed by the same routine — so ``'GDP{0:2}@MP'`` means here what it means in ``identify``:: restr = { 'FFR{0:2}@MP' , '+' ; ... 'SPARE{0:2}@MP', '+' ; ... 'GDP{0:2}@MP' , '-' ; ... 'PCEG{0:2}@MP' , '-' }; SET = rise.engine.var.lptools.identified_set(est, restr, ... draws = 1e5, ... elasticity = {'GDP','FFR',10}, ... % |GDP(0)| <= 10 |FFR(0)| normalize = {'FFR',0.05}); % 5bp impact rise ``SET.lo`` and ``SET.hi`` are ``n x (K+1) x nstate``; ``SET.naccept`` records how many draws survived in each state. ``elasticity`` bounds the ratio of two impact responses. ``normalize`` rescales every accepted draw so the named variable moves by the requested amount on impact — with it, that variable's set collapses to a point at ``k = 0``. Lag-structure restrictions are rejected: a projection has no lag polynomial to restrict. .. warning:: **A set is not a band.** Every draw satisfying the restrictions is equally admissible, so ``[lo, hi]`` is the range of responses *consistent with the restrictions* at fixed coefficients. It carries no parameter uncertainty and is not a credible interval. Reporting one as the other is the standard set-identification error. Read the **width** before the midpoint. If the two states' sets overlap almost entirely, a difference in their midpoints is not evidence of anything. And check whether a bound is pinned by a restriction rather than by the data: under ``normalize = {'FFR',0.05}`` and ``elasticity = {'GDP','FFR',10}``, a GDP set of exactly ``[-0.5, 0]`` is the restriction talking, since :math:`10 \times 0.05 = 0.5`. Running it the other way: validating against a model ------------------------------------------------------ Everything above runs data → projection → structure. The other direction is how you find out whether the specification can recover a mechanism at all: parameterise a model you trust, simulate from it, project on the artificial data, and check you get the model's own impulse responses back. A specification that cannot recover a mechanism *known* to be in the data is worthless on real data. With a recursive scheme the comparison has a hard target:: est = rise.engine.var.lptools.estimate(db, endo, horizons = 0:4); Ahat = chol(est.Sigma, 'lower'); % recovers a triangular impact matrix lp_irf_k = est.B{1}{k+1} * Ahat; % compare against the model's own IRF .. warning:: **This check is only meaningful when the shock is recoverable from the observables you hand over.** If it is not — news shocks, fiscal foresight, fewer observables than shocks — the projection converges to the wrong answer and gives no sign of it. The textbook case: :math:`y_t = \varepsilon_t + \theta\varepsilon_{t-1}` with :math:`\theta > 1` is *non-fundamental*. Its second moments are identical to those of the invertible :math:`y_t = u_t + \theta^{-1}u_{t-1}` with :math:`\mathrm{var}(u)=\theta^2`, so nothing reading only :math:`y`'s history can separate them. At :math:`\theta = 2` and :math:`T = 200{,}000` the projection returns ``0.502`` where the truth is ``2`` — it reports the effect *decaying* where it actually *grows*, and more data never helps. This is **not** a weakness of local projections specifically. Because LP and VARs estimate the same object, LP inherits the invertibility problem in full; a VAR on that data is wrong in exactly the same way. A local projection is not a robustness fix for non-fundamentalness. The way out is information, not a different estimator: supply a shock series (see shock mode above) and the same data returns ``[1, 2]`` correctly. That is the practical argument for narrative and high-frequency shock measures. One further gap worth knowing when comparing against a DSGE: a DSGE's state-space solution, projected onto a subset of observables, is generically **VARMA**, not a finite-order VAR. So agreement is only asymptotic in the lag length. At finite lags both LP and VAR are misspecified, but differently — a VAR iterates its estimated coefficients forward, so short-lag error compounds across horizons, while a projection re-estimates each horizon and it does not. That is the substantive argument for LP at long horizons, paid for in variance. Why it is cheap ---------------- Everything here is OLS and linear algebra. On seven variables, seven horizons and two states, estimation takes a small fraction of a second and 100,000 rotations take a couple of seconds. There is no posterior, so none of the mixing questions that attend a switching VAR arise. That cheapness is the main practical argument for running a local projection alongside a switching VAR rather than instead of it: the two make different assumptions and are informative about different things, and the projection costs almost nothing to add.